The basics
Develop an R package within an hour
This tutorial will teach you how to quickly create an R package. We start with a motivating example, then build an R package, and in the next part share it with the world.
Motivating example
The problem
In order to demonstrate the creation of an R-package, we will identify some odd behavior in R:
c(0.5, 1.5, 2.5, 3.5) |> round()[1] 0 2 2 4
We can see that–by default–R rounds the number 1.5 to integer 2 and the number 2.5 also to integer 2. The reason for this behavior is the IEC 60559 standard where a 5 is expected to be rounded to the even digit.
A solution
If we’d like to round up to the next integer, we can easily define the following function:
# load necessary package for between() function
library(dplyr)
# define function to load to the nearest integer
round_to_integer <- function(x){
# calculate difference between the value and the nearest integer below
diff <- x - floor(x)
# for differences < 0.5, return the nearest integer below
# for differences => 0.5, return the next nearest integer
ifelse(between(diff, 0.5, 1), ceiling(x), floor(x))
}This function rounds the vector c(0.5, 1.5, 2.5, 3.5) up to the next integer:
c(0.5, 1.5, 2.5, 3.5) |> round_to_integer()[1] 1 2 3 4
And it rounds the vector c(0.49, 1.49, 2.49, 3.49) down to the previous integer:
c(0.49, 1.49, 2.49, 3.49) |> round_to_integer()[1] 0 1 2 3
We will put this function in an R package to be able to (re)use the function and share it with others.
Creating an R package
The goal of this workshop is to wrap the function round_to_integer() into a stand-alone R package, with package documentation and a referenceable Digital Object Identifier.
Set-up the R session
We need the following packages to help us with building, testing and maintaining our package:
library(devtools) # development tools
library(usethis) # automated package and project setup
library(roxygen2) # package documentation
library(testthat) # unit testingPrepare the package structure
We need a location for our R package. The simplest approach to creating the skeleton for an R package is to use RStudio. The following gif outlines this procedure:
Alternatively, use the usethis function create_package():
usethis::create_package(path = "../roundR")As you might notice, usethis is an “incredibly chatty program”1. It sets up a new project, with a folder structure and some required documents. We will go over the relevant folders and documents one-by-one.
✔ Creating ../roundR/.
✔ Setting active project to "/path/on/your/machine/roundR".
✔ Creating R/.
✔ Writing DESCRIPTION.
Package: roundR
Title: What the Package Does (One Line, Title Case)
Version: 0.0.0.9000
Authors@R (parsed):
* First Last <first.last@example.com> [aut, cre]
Description: What the package does (one paragraph).
License: `use_mit_license()`, `use_gpl3_license()` or friends to
pick a license
Encoding: UTF-8
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.3
✔ Writing NAMESPACE.
✔ Writing roundR.Rproj.
✔ Adding "^roundR\\.Rproj$" to .Rbuildignore.
✔ Adding ".Rproj.user" to .gitignore.
✔ Adding "^\\.Rproj\\.user$" to .Rbuildignore.
✔ Opening /path/on/your/machine/roundR/ in a new session.
✔ Setting active project to "<no active project>".
Explore the package structure
We have now created the necessary structure for an R package. We can see this structure and the generated package files in the File Pane in RStudio.
The R subfolder contains all the R code for your package and the man folder contains all the corresponding function documentation (i.e., what is shown using the help() function in R).
By default, for a new R package generated by RStudio the file hello.R is generated inside the R directory.
As a matter of fact, the skeleton R package is already a fully functional R package. Try installing the package in the Build pane.
Edit the description
Open the file DESCRIPTION from the file pane. The following window opens:
Now, replace the contents of the DESCRIPTION file with
Package: roundR
Type: Package
Title: Round Numeric Values to the Nearest Integer
Version: 0.0.1
Authors@R: c(
person(
"Hanne", "Oberman",
email = "h.i.oberman@uu.nl",
role = c("aut", "cre")
),
person(
"Gerko", "Vink",
email = "g.vink@uu.nl",
role = c("aut")
)
)
Description: By default, R rounds numeric values to even integers, following
the IEC 60559 standard. This package offers alternative functionality to
round to the closest integer.
License: What license is it under?
Encoding: UTF-8
LazyData: trueThe DESCRIPTION file governs the information about the licence, authors, contributors, maintainers, etc. The argument lazyData: true indicates that data sets should be lazily loaded. This means that data will not occupy any memory unless it is needed. This is a good argument to have as default.
We also need to set a license. Running the following code from package usethis will write a permissive MIT license to the description file:
usethis::use_mit_license()To learn more about licencing, see the R Packages book.
Don’t forget to save the updated DESCRIPTION file and change our names to your name.
Add the functional code
Now it is time to extend the package with the functionality we promised in our updated DESCRIPTION. To do so, we can either manually add a new file in the R folder called round_to_integer.R, or do it programmatically:
usethis::use_r("round_to_integer")The use_r() function from the usethis package is very convenient, because it creates the necessary file in the correct location and opens the file in the editor pane.
We can now copy our own rounding function to the round_to_integer.R file:
round_to_integer <- function(x){
# calculate difference between the value and the nearest integer below
diff <- x - floor(x)
# for differences < 0.5, return the nearest integer below
# for differences => 0.5, return the next nearest integer
ifelse(between(diff, 0.5, 1), ceiling(x), floor(x))
}Set-up the package documentation
Now that we have created the file for our functional code, and added the code itself, we should make the code file recognizable by R as a package function by adding function documentation.
The most flexible approach to creating and maintaining package documentation is to use “roxygen”, a special kind of code commenting style. The roxygen2 package is a convenient in-line documentation convention that generates a bunch of machine-readable files that are required for R packages, such as the NAMESPACE file and help files (one for each function in your package; collected as .Rd files in the man folder).
The man folder contains all documentation files. However, you can imagine if we have two separate locations for our R code and our Rd help files, that at some point the code and documentation might get out of sync. For example, if we update the code, but forget to reflect changes in our manual, the usability of our package may be at stake and documentation to end-users might get confusing. Most of all, it would be a lot of work for us to maintain multiple linked files in multiple locations. roxygen2 solves this for us by extracting the documentation from our R code file. The only thing we need to do is maintain a single file: the code file in the R folder with some special code comments in roxygen style.
A minimal example of roxygen code comments would be as follows, matching an R function with arguments x and y:
#' A short description of your function
#'
#' @param x The expected input for the first function argument, x.
#' @param y Input for the second argument denoted by the letter y.
#' @returns A short description of the expected function output.
To start with roxygen2 in our package, we need to instruct the package to start using roxygen2:
usethis::use_roxygen_md()The above call will add the following two lines to our DESCRIPTION file, indicating that the package is documented using roxygen2.
RoxygenNote: 7.3.3
Roxygen: list(markdown = TRUE)
Write the function documentation
In RStudio, it is very easy to automatically insert the necessary roxygen code comments into to your function file. Place your cursor anywhere in the function code (click inside the round_to_integer() function definition in the round_to_integer.R file). Then click Code \(\rightarrow\) Insert Roxygen Skeleton to automatically create the relevant roxygen documentation. The following code comments will appear:
#' Title
#'
#' @param x
#'
#' @returns
#' @export
#'
#' @examples
Filling in the roxygen skeleton–with some customization and examples–could result in the following round_to_integer.R code file:
#' Round to the nearest integer
#'
#' The \code{\link{base::round}} function rounds the number `1.5` to
#' `2` and the number `2.5` also to `2`, because of the IEC 60559 standard.
#' This function provides an integer rounding alternative to
#' \code{\link{base::round}}.
#'
#' @param x A numeric element or vector to round to the nearest integer
#' @returns An integer element or vector
#' @author Gerko Vink \email{g.vink@uu.nl} and Hanne Oberman
#' \email{h.i.oberman@uu.nl}
#'
#' #' @examples
#' # unexpected rounding
#' c(0.5, 1.5, 2.5, 3.5) |> round()
#' # rounding to nearest integer
#' c(0.5, 1.5, 2.5, 3.5) |> round_to_integer()
#'
#' @export
round_to_integer <- function(x){
# calculate difference between the value and the nearest integer below
diff <- x - floor(x)
# for differences < 0.5, return the nearest integer below
# for differences => 0.5, return the next nearest integer
ifelse(between(diff, 0.5, 1), ceiling(x), floor(x))
}You can copy the roxygen comments to your round_to_integer.R file. A good source to find inspiration for writing roxygen2 documentation is the roxygen2 reference page.
Generate the package documentation
Now that we have a working round_to_integer.R file with documentation code included, we can build the documentation files. A good wrapper function to render all documentation in your package is the following code evaluation:
devtools::document()The document() function from the devtools package will build all documentation for all files that use roxygen2 and it will build the NAMESPACE of your package accordingly.
If you encounter the following message:
Skipping NAMESPACE
✖ It already exists and was not generated by roxygen2.
just remove the NAMESPACE file and re-run the document() function.
By now, your function should have a corresponding help file in the man folder, round_to_integer.Rd. We will remove the R/hello.R and man/hello.Rd files as they are not supposed to be part of our package.
Delete the file hello.R from the R folder manually or with:
file.remove("./R/hello.R")Re-render the package documentation to remove the function help file too:
devtools::document()Evaluating the R package
To find out whether we’ve created a functioning R package, we can use the built-in checking tools in RStudio or the devtools function check(). The check() function executes an evaluation suite similar to the ones for CRAN. Whichever method you choose, they all result in an R CMD check, which is “the official method for checking that an R package is valid”.
Run package check
Let’s check the functionality of our package by clicking Check in the Build tab, or running:
devtools::check()We can see that our current package yields 1 error, 1 warning and 1 note:
1 error ✖ | 1 warning ✖ | 1 note ✖
Investigate the error
The error message is as follows:
❯ checking examples ... ERROR
Running examples in ‘roundR-Ex.R’ failed
The error most likely occurred in:
> base::assign(".ptime", proc.time(), pos = "CheckExEnv")
> ### Name: round_to_integer
> ### Title: Round to the nearest integer
> ### Aliases: round_to_integer
>
> ### ** Examples
>
> # unexpected rounding
> c(0.5, 1.5, 2.5, 3.5) |> round()
[1] 0 2 2 4
> # rounding to nearest integer
> c(0.5, 1.5, 2.5, 3.5) |> round_to_integer()
Error in between(diff, 0, 0.5) : could not find function "between"
Calls: round_to_integer -> ifelse
Execution halted
The error arises from the examples in our documentation. But the issue itself lies within the round_to_integer() function code. Inside the function, we use the between() function from the dplyr package. But we did not explicitly load that package before running the example.
The examples are self-contained, meaning that any dependent packages need to be explicitly loaded via library() for the code to run, just like any other R instance. Simply adding library(dplyr) to the example code will solve the error.
Navigate to the round_to_integer.R file and add library(dplyr) to the examples. The examples now contain:
#' @examples
#' library(dplyr)
#' # unexpected rounding
#' c(0.5, 1.5, 2.5, 3.5) |> round()
#' # rounding to nearest integer
#' c(0.5, 1.5, 2.5, 3.5) |> round_to_integer()Investigate the warning
The warning message is as follows:
❯ checking Rd cross-references ... WARNING
Missing link(s) in Rd file 'round_to_integer.Rd':
‘base::round’
See section 'Cross-references' in the 'Writing R Extensions' manual.
Found the following Rd file(s) with Rd \link{} targets missing package
anchors:
round_to_integer.Rd: base::round
Please provide package anchors for all Rd \link{} targets not in the
package itself and the base packages.
The error stems from the cross-reference we attempted in our documentation. The proper way to refer to function round() from package base is not with \link{base::round}, but with [base::round()] following the structure [otherpkg::otherfunction()]. We could have opted for not referencing or linking the round() function at all, but this would not align with open and inclusive development conventions.
Correct the faulty cross-references in the documentation:
#' The [base::round()] function rounds the number `1.5` to `2` and the number
#' `2.5` also to `2`, because of the IEC 60559 standard. This function provides
#' an integer rounding alternative to [base::round()].Investigate the note
The note message is as follows:
❯ checking R code for possible problems ... NOTE
round_to_integer: no visible global function definition for ‘between’
Undefined global functions or variables:
between
We use function between() from package dplyr, but we neglected to make explicit that the function between() is required for our function round_to_integer() to work. In other words, R needs to know that our package roundR would depend on package dplyr for its functionality to work.
We can fix this in two steps: 1) Tell R explicitly that we are using the between() function from the dplyr package in the definition of our function by adding dplyr:: to the function body:
round_to_integer <- function(x){
# calculate difference between the value and the nearest integer below
diff <- x - floor(x)
# for differences < 0.5, return the nearest integer below
# for differences => 0.5, return the next nearest integer
ifelse(dplyr::between(diff, 0.5, 1), ceiling(x), floor(x))
}and 2) Add dplyr as a dependency to our package by importing the required between() function:
usethis::use_import_from("dplyr", "between")When asked:
! `use_import_from()` requires package-level documentation.
Would you like to add it now?
1: No way
2: Not now
3: For sure
Selection:
Enter an item from the menu, or 0 to exit
choose the fun option that would add it (i.e. For Sure, Yes, Absolutely, etc). These options change every time you re-run the function code, so read it carefully!
In the folder R, there should now be a new file roundR-package.R with some roxygen code, which means we have to re-run devtools::document() to update our documentation.
Re-render documentation
Update the package documentation with:
devtools::document()Evaluate what happened. What has changed in our DESCRIPTION and NAMESPACE files? If you want to learn more about R package dependencies, see the Dependencies and Imports section in the R Packages book.
Re-run checks
Try re-running the check() to see if you hit three green check marks (no more errors, warnings and notes):
devtools::check()If all is well, you’ll see:
── R CMD check results ───────────────────────────────── roundR 0.1.0 ────
Duration: 5.7s
0 errors ✔ | 0 warnings ✔ | 0 notes ✔
R CMD check succeeded
This means that, in principle, you could go ahead and submit this package to CRAN. But let’s first try and use our new package locally.
Try the package
During R package development, you will probably often try and see whether your code runs as expected–specifically whether your functions work as you’d hoped. Your new favorite function might be devtools::load_all().
Load the current development version of the package:
devtools::load_all()Run some code to try our function from the loaded package, for example:
round_to_integer(2.5)Instead of running R code manually to verify whether our function performs as expected, we can automate this with tests.
Add tests
The next step for a mature package is to include tests. Every function should have functional tests. The testthat package is geared to that. Make sure that you have the round_to_integer.R file open in the Source pane and run:
usethis::use_test()The proper structure for test files has now been created:
✔ Adding 'testthat' to Suggests field in DESCRIPTION
✔ Setting Config/testthat/edition field in DESCRIPTION to '3'
✔ Creating 'tests/testthat/'
✔ Writing 'tests/testthat.R'
✔ Writing 'tests/testthat/test-round_to_integer.R'
• Modify 'tests/testthat/test-round_to_integer.R'
You are asked to modify the tests/testthat/test-round_to_integer.R file. Replace the example test with:
test_that("round_to_integer works", {
A <- c(0.5, 1.5, 2.5, 3.5) |> round_to_integer()
B <- c(0.49, 1.49, 2.49, 3.49) |> round_to_integer()
expect_equal(A, c(1, 2, 3, 4))
expect_equal(B, c(0, 1, 2, 3))
})
test_that("round_to_integer yields different results than round", {
vec1 <- c(0.5, 1.5, 2.5, 3.5)
vec2 <- c(
0.499999999999999999995,
1.499999999999999999995,
2.499999999999999999995,
3.499999999999999999995
)
A <- vec1 |> round_to_integer()
B <- vec1 |> round()
C <- vec2 |> round_to_integer()
D <- vec2 |> round()
expect_false(identical(A, B))
expect_false(identical(C, D))
})Click the Test button in the Build pane, or run:
devtools::test()All test should pass, meaning that your round_to_integer() function yields correct results (test 1) that differ fundamentally from the results obtained with round() (test 2).
If test would fail, you’d be notified. For example
test_that("round_to_integer works", {
A <- c(0.5, 1.5, 2.5, 3.5) |> round_to_integer()
expect_equal(A, c(4, 3, 2, 1))
})── Failure: round_to_integer works ─────────────────────────────────────────────
`A` not equal to c(4, 3, 2, 1).
4/4 mismatches (average diff: 2)
[1] 1 - 4 == -3
[2] 2 - 3 == -1
[3] 3 - 2 == 1
[4] 4 - 1 == 3
Error:
! Test failed
Fortunately, our test all passed. Now re-run the R CMD check. If all is well you’ll receive confirmation of a successful check.
Increase the version
Now that we have a working package with successful checks, we might think about updating the version of the package. After all, a lot has changed since the package got defined at the start of our development journey.
The easiest means to increasing the version to 0.1.0 (indicating a minor update) is to use
usethis::use_version(which = "minor")You will be presented with something like:
✔ Adding "0.1.0" to Version.
ℹ There is 1 uncommitted file:
• DESCRIPTION
! Is it ok to commit it?
1: Definitely
2: Absolutely not
3: Negative
Selection:
The question Is it ok to commit it? is related to git. Please select the affirmative option (such as Definitely) to commit the version increase. For reasons of brevity and simplicity, we will leave a thorough discussion of incremental git commits for now and demonstrate to use of git and GitHub at the end of this walk through.
Install your new package
Install the package by clicking Install in the Build tab. In the console, the following should occur:
Restarting R session...
> library(roundR)
Your package is now ready to use locally, but not yet ready to share with the world… That’s what the next part is all about!
—
Copyright Hanne Oberman, 2025 - CC BY-NC-SA 4.0
Materials developed by Amices team - Methodology & Statistics - Utrecht University
Footnotes
Quote by
usethisdeveloper Jenny Bryan↩︎